package network

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/go-playground/validator/v10"
	proto2 "github.com/golang/protobuf/proto"
	"github.com/pkg/errors"
	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc"
	"google.golang.org/grpc/encoding"
	"google.golang.org/grpc/encoding/proto"
	"google.golang.org/grpc/peer"
	"google.golang.org/grpc/status"
	"os"
	"pay/pkg/ecode"
	"pay/pkg/env"
	"pay/pkg/log"
	"pay/pkg/network/jsonpb"
	"pay/pkg/request_key"
	"pay/pkg/token"
	"pay/pkg/wechat"
	"strings"
	"time"
)

var (
	Validator = validator.New()
	marshaler = &jsonpb.Marshaler{}
)

// 错误包装 + 防止结构性异常触发服务终止
func errorInterceptor(envType env.Type, serviceName string) grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		ctx = request_key.UpdateContextWithMetaRequestID(ctx)
		defer func() {
			if r := recover(); r != nil {
				stackTrace := ecode.GetPanicTrace(fmt.Sprintf("rpc outer %v", req), r)
				if envType != env.Local {
					log.FromContext(ctx).Error(stackTrace)
					body, _ := json.Marshal(req)
					wechat.AsyncSendMarkDown(
						ctx, wechat.FormatBugMessage(ctx, envType, serviceName, info.FullMethod, string(body), stackTrace))
				} else {
					_, _ = fmt.Fprintf(os.Stderr, stackTrace)
				}
				err = ecode.ErrSystemError
			}
			if err != nil {
				if e, ok := err.(*ecode.ErrorCode); ok {
					rpcStatus := status.New(e.ErrorType.GRPCCode, e.ErrMessage)
					rpcStatus, _ = rpcStatus.WithDetails(&errdetails.ErrorInfo{
						Reason: e.ErrorType.ErrType,
						Metadata: map[string]string{
							"ErrorCode": e.CodeMessage, "ErrorMessage": e.ErrMessage},
					})
					err = rpcStatus.Err()
				}
			}
		}()
		resp, err = handler(ctx, req)
		return
	}
}

// GetHttpCallInfo
func GetHttpCallInfo(ctx context.Context) (bool, string, string) {
	method := request_key.GetSingleMetaData(ctx, httpMethodKey, "rpc")
	if method == "rpc" {
		return false, "rpc", method
	} else {
		return true, "http", method
	}
}

// 日志
func (n *NetWork) logInterceptor(envType env.Type, serviceName string,
	errorFormatter func(*ecode.ErrorCode) proto2.Message,
	tokenInterceptor func(context.Context, interface{}) (context.Context, error)) grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		startTime := time.Now()
		path := request_key.GetSingleMetaData(ctx, httpPathKey, info.FullMethod)
		defer func() {
			duration := time.Since(startTime)
			var (
				msg, method, remoteIP, errString, stackTrace, body, respBody string
				logFn                                                        func(msg string, keysAndValues ...interface{})
				isHttp                                                       bool
				// 状态关键字，用于日志grep & kibana查询
				statusCode string
			)
			if peerInfo, ok := peer.FromContext(ctx); ok {
				remoteIP = peerInfo.Addr.String()
			}
			logger := log.FromContext(ctx)
			if r := recover(); r != nil {
				logFn = logger.Warnw
				errString = fmt.Sprintf("%s", r)
				stackTrace = ecode.GetPanicTrace("rpc", r)
				statusCode = "Fail-Panic"
			} else if err != nil {
				logFn = logger.Warnw
				serverErr := errors.Cause(err)
				switch v := serverErr.(type) {
				case *ecode.ErrorCode:
					errString = fmt.Sprintf("%s-%s", v.CodeMessage, v.ErrMessage)
					statusCode = fmt.Sprintf("Fail-%s", v.ErrorType)
					err = serverErr
				case ecode.WithStackModel:
					stackTrace = fmt.Sprintf("error: %+v", err)
					errString = "系统错误"
					err = serverErr
					statusCode = "Fail-Unknown"
				default:
					switch serverErr {
					case context.Canceled:
						err = ecode.ErrCancelled
						errString = "用户取消"
						statusCode = "Fail-Cancelled"
					case context.DeadlineExceeded:
						statusCode = "Fail-DeadlineExceeded"
						errString = "服务中止"
						stackTrace = fmt.Sprintf("error: service aborted, duration=%s", duration)
					default:
						// 错误未填充堆栈信息
						stackTrace = fmt.Sprintf("error: %s (no stack filled)", err.Error())
						errString = "系统错误"
						statusCode = "Fail-Unknown"
					}
				}
			} else {
				logFn = logger.Infow
				statusCode = "Success"
			}
			// call server handler
			isHttp, msg, method = GetHttpCallInfo(ctx)
			if method != "GET" {
				protoReq, _ := req.(proto2.Message)
				body, _ = marshaler.MarshalToString(protoReq)
			}
			t := token.GetTokenFromContext(ctx)
			if stackTrace != "" {
				err = ecode.ErrSystemError
				if envType != env.Local {
					if isHttp {
						wechat.AsyncSendMarkDown(
							ctx, wechat.FormatBugMessage(
								ctx, envType, serviceName, path, body, stackTrace))
					} else {
						wechat.AsyncSendMarkDown(
							ctx, wechat.FormatBugMessage(
								ctx, envType, serviceName, info.FullMethod, body, stackTrace))
					}
				} else {
					_, _ = fmt.Fprintf(os.Stderr, stackTrace)
				}
			} else if envType != env.Production && err == nil {
				protoResp, _ := resp.(proto2.Message)
				respBody, _ = marshaler.MarshalToString(protoResp)
			}
			if err != nil {
				errObj, ok := err.(*ecode.ErrorCode)
				if !ok {
					errObj = ecode.ErrSystemError
				}
				gst, _ := status.New(errObj.ErrorType.GRPCCode, errObj.ErrMessage).WithDetails(
					errorFormatter(errObj))
				err = gst.Err()
			}
			if strings.Contains(path, "ping") {
				// 屏蔽健康检查日志
				return
			}
			logFn(msg,
				"status_code", statusCode,
				"latency", duration.Seconds(),
				"client_ip", remoteIP,
				"method", method,
				"path", path,
				"body", body,
				"resp", respBody,
				"token_key", t.Digest,
				"token", t.Raw,
				"error", errString,
				"stacktrace", stackTrace,
			)
		}()
		// token检查
		// token检查
		if ctx, err = tokenInterceptor(ctx, req); err != nil {
			return
		}
		// 参数检查
		if err = Validator.Struct(req); err != nil {
			err = ecode.BadRequest("参数错误: error=%s", err.Error())
			return
		}
		resp, err = handler(ctx, req)
		if err == nil {
			// 强制执行一次底层send_response的Marshal，规避server强制退出的风险，确保recover能捕捉
			_, err = encoding.GetCodec(proto.Name).Marshal(resp)
		}
		return
	}
}

func ClientInterceptor(serviceName string, timeout time.Duration,
	errorTranslator func(error) *ecode.ErrorCode) grpc.UnaryClientInterceptor {
	return func(ctx context.Context, method string, req interface{}, reply interface{},
		cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
		start := time.Now()
		var clientCtx context.Context
		if deadline, ok := ctx.Deadline(); !ok {
			// 未设置超时时间，取全局超市是啊金
			invokeCtx, cancel := context.WithTimeout(request_key.NewContextWithReqID(ctx), timeout)
			defer cancel()
			clientCtx = invokeCtx
		} else {
			// 已设置独立超时时间，跟随独立超时时间
			clientCtx = ctx
			timeout = deadline.Sub(start)
		}
		err := invoker(clientCtx, method, req, reply, cc, opts...)
		var statusCode, errString string
		if err != nil {
			if errors.Is(clientCtx.Err(), context.DeadlineExceeded) {
				err = ecode.BizError(
					"REMOTE_ERROR", "下游服务%s调用超时", serviceName)
				statusCode = "Fail-DeadlineExceeded"
				errString = fmt.Sprintf("timeout-请求超时,超时设置=%f秒", timeout.Seconds())
			} else if errors.Is(clientCtx.Err(), context.Canceled) {
				err = ecode.ErrCancelled
				statusCode = "Fail-Cancelled"
				errString = fmt.Sprintf("timeout-请求取消,超时设置=%f秒", timeout.Seconds())
			} else {
				err = errorTranslator(err)
				errCode := err.(*ecode.ErrorCode)
				statusCode = fmt.Sprintf("Fail-%s", errCode.ErrorType)
				errString = fmt.Sprintf("%s-%s", errCode.CodeMessage, errCode.ErrMessage)
			}
		} else {
			statusCode = "Success"
		}
		protoReq, _ := req.(proto2.Message)
		protoReply, _ := reply.(proto2.Message)
		jsonReq, _ := marshaler.MarshalToString(protoReq)
		jsonReply, _ := marshaler.MarshalToString(protoReply)
		log.FromContext(ctx).Infow(serviceName,
			"status_code", statusCode,
			"latency", time.Now().Sub(start).Seconds(),
			"method", "rpc",
			"path", method,
			"body", jsonReq,
			"resp", jsonReply,
			"error", errString,
		)
		return err
	}
}
